// Copyright © 2017 NAME HERE <EMAIL ADDRESS>
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
	"crypto/tls"
	"flag"
	"fmt"
	"go_template/certs"
	"go_template/service"
	"go_template/store"
	"io"

	"golang.org/x/net/context"

	"net"
	"net/http"
	"path"
	"strings"

	mgo "gopkg.in/mgo.v2"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"

	pb "go_template/proto"

	"go_template/log"

	"github.com/golang/glog"
	"github.com/grpc-ecosystem/go-grpc-middleware"
	"github.com/grpc-ecosystem/go-grpc-middleware/recovery"
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"github.com/spf13/cobra"
)

// serveCmd represents the serve command
var serveCmd = &cobra.Command{
	Use:     "serve",
	Aliases: []string{"s"},
	Short:   "A brief description of your command",
	Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("serve called")
		serve(&serverAddr{"127.0.0.1", "8080"})
	},
}

func init() {
	rootCmd.AddCommand(serveCmd)
}

type serverAddr struct {
	Host string
	Port string
}

func (addr *serverAddr) Network() string {
	return "tcp"
}

func (addr *serverAddr) String() string {
	return strings.Join([]string{addr.Host,
		addr.Port}, ":")
}

func serve(addr net.Addr) {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	grpcServer := newGrpcServer(addr)

	mux := http.NewServeMux()
	mux.HandleFunc("/swagger.json", func(w http.ResponseWriter, req *http.Request) {
		io.Copy(w, strings.NewReader(pb.Swagger))
	})

	gwmux, err := newGateway(ctx, addr)
	if err != nil {
		panic(err)
	}
	mux.Handle("/", gwmux)

	conn, err := net.Listen(addr.Network(), addr.String())
	if err != nil {
		panic(err)
	}

	srv := &http.Server{
		Addr:    addr.String(),
		Handler: grpcHandlerFunc(grpcServer, mux),
		TLSConfig: &tls.Config{
			Certificates: []tls.Certificate{*certs.CertKeyPairs},
			NextProtos:   []string{"h2"},
		},
	}

	err = srv.Serve(tls.NewListener(conn, srv.TLSConfig))

	if err != nil {
		log.ShowLogger.Fatal("ListenAndServe: ", err)
	}
	return
}

var (
	swaggerDir = flag.String("swagger_dir", "go_template/proto", "path to the directory which contains swagger definitions")
)

func serveSwagger(w http.ResponseWriter, r *http.Request) {
	if !strings.HasSuffix(r.URL.Path, ".swagger.json") {
		glog.Errorf("Not Found: %s", r.URL.Path)
		http.NotFound(w, r)
		return
	}

	glog.Infof("Serving %s", r.URL.Path)
	p := strings.TrimPrefix(r.URL.Path, "/swagger/")

	p = path.Join(*swaggerDir, p)
	http.ServeFile(w, r, p)
}

// grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
// connections or otherHandler otherwise. Copied from cockroachdb.
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		/*
			w.Header().Add("Access-Control-Allow-Credentials", "true")*/

		// TODO(tamird): point to merged gRPC code rather than a PR.
		// This is a partial recreation of gRPC's internal checks https://github.com/grpc/grpc-go/pull/514/files#diff-95e9a25b738459a2d3030e1e6fa2a718R61
		if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
			grpcServer.ServeHTTP(w, r)
		} else {
			if origin := r.Header.Get("Origin"); origin != "" {
				w.Header().Set("Access-Control-Allow-Origin", origin)
				if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" {

					w.Header().Set("Access-Control-Allow-Methods", "POST,GET,HEAD,DELETE")
					w.Header().Add("Access-Control-Allow-Headers", "x-requested-with,Content-Type, Accept,content-type")
					w.Header().Add("Content-Type", "application/json") //返回数据格式是json
					return
				}
			}
			otherHandler.ServeHTTP(w, r)
		}
	})
}

func newGrpcServer(addr net.Addr) *grpc.Server {
	opts := []grpc.ServerOption{
		grpc.Creds(credentials.NewClientTLSFromCert(certs.Certpool, addr.String()))}
	opts = append(opts, grpc_middleware.WithUnaryServerChain(grpc_recovery.UnaryServerInterceptor(grpc_recovery.WithRecoveryHandler(recoverPanic)),
		grpc.UnaryServerInterceptor(UnaryServerInterceptor)))
	opts = append(opts)
	grpcServer := grpc.NewServer(opts...)
	pb.RegisterEchoServiceServer(grpcServer, service.NewEchoService())

	return grpcServer
}

// newGateway returns a new gateway server which translates HTTP into gRPC.
func newGateway(ctx context.Context, addr net.Addr, opts ...runtime.ServeMuxOption) (http.Handler, error) {
	dcreds := credentials.NewTLS(&tls.Config{
		ServerName: addr.String(),
		RootCAs:    certs.Certpool,
	})
	gwmux := runtime.NewServeMux()
	dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}

	err := pb.RegisterEchoServiceHandlerFromEndpoint(ctx, gwmux, addr.String(), dopts)
	if err != nil {
		return nil, err
	}
	return gwmux, nil
}

func recoverPanic(p interface{}) error {
	return grpc.Errorf(codes.Internal, "%s", codes.Internal.String())
}

func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	log.ShowLogger.Debug("before handling. Info: %+v", info)
	defer log.ShowLogger.Sync()

	session, err := store.CopySession()
	if err != nil {
		log.ShowLogger.Info("connect db error")
		return codes.Internal, nil
	}
	defer session.Close()

	// Optional. Switch the session to a monotonic behavior.
	session.SetMode(mgo.Monotonic, true)
	newCtx := context.WithValue(ctx, "user_id", "sss")
	newCtx = context.WithValue(newCtx, "db", session)
	resp, err := handler(newCtx, req)

	log.ShowLogger.Debug("after handling. resp: %+v", resp)
	return resp, err
}
